#! /usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (c) 2009 Matías Ribecky <mail@mribecky.com.ar>
#
# This file is part of pyformatter.
# 
# pyformatter is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# pyformatter is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with pyformatter.  If not, see <http://www.gnu.org/licenses/>.

import re
import sys
import difflib
import optparse
import itertools
import functools

def main(argv):
    filters, input_file, output_file, show_diff = parse_args(argv)

    code, original = itertools.tee(input_file, 2)
    for code_filter in filters:
        code = code_filter(code)

    if not show_diff:
        for line in code:
            output_file.write(line)
    else:
        diff = difflib.ndiff(list(original), list(code))
        sys.stderr.write(''.join(diff))
    return 0

def parse_args(argv):
    arg_parser = create_arg_parser()
    opts, _ = arg_parser.parse_args(argv[1:])

    filters = [ExpandTabs(opts.indent_size),
               join_braked_lines,
               normalize_spaces,
               IndentNormalizer(opts.indent_size),
               LineBreaker(opts.line_size, opts.indent_size)
               ]

    if opts.input_file_name != '-':
        input_file = open(opts.input_file_name)
    else:
        input_file = sys.stdin

    if opts.output_file_name != '-':
        output_file = open(opts.output_file_name)
    else:
        output_file = sys.stdout

    return filters, input_file, output_file, opts.diff

def create_arg_parser():
    arg_parser = optparse.OptionParser(usage="%prog [options]")
    arg_parser.add_option('-l',
                          '--line-size',
                          action='store',
                          type='int',
                          default=79,
                          help="Line width"
                          )
    arg_parser.add_option('-n',
                          '--indent-size',
                          action='store',
                          type='int',
                          default=4,
                          help="Indent width"
                          )
    arg_parser.add_option('-i',
                          '--input',
                          action='store',
                          default='-',
                          dest='input_file_name',
                          help="Input file name, defaults to stdin"
                          )
    arg_parser.add_option('-o',
                          '--output',
                          action='store',
                          default='-',
                          dest='output_file_name',
                          help="Output file name, defaults to stdout"
                          )
    arg_parser.add_option('-d',
                          '--diff',
                          action='store_true',
                          default=False,
                          help="Show the changes on stderr"
                          )
    return arg_parser

class ExpandTabs:
    def __init__(self, tab_size):
        self.tab_size = tab_size

    def __call__(self, code):
        for line in code:
            yield line.replace('\t', ' ' * self.tab_size)

_operators = '=+-*/%'
def normalize_spaces(code):
    for line in code:
        if line.lstrip().startswith('#'):
            # Skip comments
            yield line
            continue

        for operator in _operators:
            line = re.sub('\s*\\' + operator + '\s*',
                          ' ' + operator + ' ',
                          line)

        # Argument unpacking 'asd(1, *args)
        line = re.sub(r',\s*\*\s*', r', *', line)
        line = re.sub(r'\(\s*\*\s*', r', *', line)
        # Keyword argument unpacking 'asd(1, **kwargs)
        line = re.sub(r'\*\s*\*\s*', r'**', line)

        # Parentesis spacing
        line = re.sub(r'(\w)\s*\(\s*', r'\1(', line)
        line = re.sub(r'(\W)\s*\(\s*', r'\1 (', line)
        line = re.sub(r'\s*\)\s*', r')', line)

        # Comma separated values
        line = re.sub(r'\s*,\s*', r', ', line)

        # Colon
        line = re.sub(r'\s*:\s*', r': ', line)

        # Non indent multiple spaces
        line = re.sub(r'(\S)\s\s+', r'\1 ', line)

        yield line.rstrip() + '\n'

class IndentNormalizer:
    def __init__(self, indent_size):
        self.indent_size = indent_size

    def __call__(self, code):
        indent_sizes = []
        for line in code:
            if not line.strip():
                # skip empty lines
                yield '\n'
                continue

            last_indent = indent_sizes[-1] if indent_sizes else 0
            new_indent = len(list(itertools.takewhile(lambda c: c in ' \t',
                                                      line)))
            if new_indent > last_indent:
                # new level
                indent_sizes.append(new_indent)
            elif new_indent < last_indent:
                # dedent
                while indent_sizes and new_indent < indent_sizes[-1]:
                    indent_sizes.pop()
            yield ' ' * self.indent_size * len(indent_sizes) + line[new_indent:]

def join_braked_lines(code):
    for line in code:
        for open_bracket, close_bracket in ('()', '[]', '{}'):
            while line.count(open_bracket) != line.count(close_bracket):
                line = line[:-1] + code.next()
        yield line

class LineBreaker(object):
    """Break lines longer that line_width."""

    def __init__(self, line_width, indent_size):
        self.line_width = line_width
        self.indent_size = indent_size

    def __call__(self, code):
        for line in code:
            if len(line) > self.line_width:
                for new_line in self.break_line(line):
                    yield new_line
            else:
                yield line

    def break_line(self, line):
        brackets = self.parse_brackets(line)
        if brackets:
            return self.break_brackets(line, brackets)
        else:
            return self.break_no_brackets(line)

    def break_brackets(self, line, brackets):
        brackets = list(sorted(brackets, key=lambda bracket: bracket.start))
        if self._brackets_are_nested(brackets):
            # If they are all nested brackets, break the innermost that starts
            # before the line_width.
            for bracket in reversed(brackets):
                if bracket.start <= self.line_width:
                    return self.break_bracket(line, bracket)
        else:
            return line

    def break_bracket(self, line, bracket):
        if bracket.start < (self.line_width / 2):
            # brake putting one item below the other, as in:
            # l = ['a',
            #      'b',
            #      'c'
            #      ]
            indent = bracket.start + 1

            it = iter(bracket.items)
            last_item_pos = it.next()
            yield line[:last_item_pos + 1].rstrip() + '\n'
            for item_pos in it:
                item = line[last_item_pos + 1:item_pos + 1].strip()
                yield ' ' * indent + item + '\n'
                last_item_pos = item_pos
            item = line[last_item_pos + 1:].strip()
            yield ' ' * indent + item[:-1] + '\n'
            yield ' ' * indent + item[-1] + '\n'
        else:
            # brake putting items in the same line, as in:
            # some_very_very_very_long_prefix(
            #    'a', 'b', 'c', ...
            #    'y', 'z')
            yield line[:bracket.start + 1].rstrip() + '\n'
            last_break = bracket.start + 1

            indent = len(list(itertools.takewhile(lambda c: c in ' \t', line)))
            indent += self.indent_size

            last_item_pos = last_break
            for item_pos in bracket.items:
                if indent + (item_pos - last_break) > self.line_width:
                    items = line[last_break: last_item_pos + 1].strip()
                    yield ' ' * indent + items + '\n'
                    last_break = last_item_pos + 1
                last_item_pos = item_pos
            items = line[last_break:].strip()
            yield ' ' * indent + items + '\n'

    def parse_brackets(self, line):
        brackets = []
        stack = []
        for pos, c in enumerate(line):
            if c in '([{':
                stack.append(Bracket(c, pos))
            elif c in ')]}':
                # this wil not do good with wrongly balanced brackets
                bracket = stack.pop()
                bracket.end = pos
                brackets.append(bracket)
            elif c in ',':
                if stack:
                    stack[-1].add_item_pos(pos)
        return brackets

    def _brackets_are_nested(self, brackets):
        for bracket1, bracket2 in itertools.permutations(brackets, 2):
            if bracket1.start < bracket2.start:
                if bracket1.end < bracket2.end:
                    return False
            elif bracket1.start > bracket2.start:
                if bracket1.end > bracket2.end:
                    return False
        return True

class Bracket(object):
    def __init__(self, start_char, start):
        self.start_char = start_char
        self.start = start
        self.end = None
        self.items = []

    def add_item_pos(self, pos):
        self.items.append(pos)


if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))
